package engine.individuals;

import its.model.environment.crossroad.Program;
import its.model.environment.crossroad.ProgramPhase;
import its.opt.program.ProgramBuilder;
import its.util.RandomFactory;
import its.util.TimeUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

public class ProgramedTrafficNetIndividual implements TrafficNetIndividual<Integer>, Cloneable {

	/** Logger fo this class. */
	private static final Logger LOGGER = Logger.getLogger(ProgramedTrafficNetIndividual.class.getName());
	
	private static final int BITS_PER_INTEGER = 6;
	public static final int MAX_PHASE_LENGTH = ProgramBuilder.MAX_PHASE_LENGTH;
	public static final int MIN_PHASE_LENGTH = ProgramBuilder.MIN_PHASE_LENGTH;
	private Program[] programs;
	
	/**
	 * Cache for number of phases. <b>Do not change the current programs</b> 
	 * ({@link ProgramedTrafficNetIndividual#programs})
	 */
	private int phasesCount;

	public ProgramedTrafficNetIndividual(Program[] programs) {
		this.programs = programs;
		this.phasesCount = 0;
	}

	public Integer getProgramVariableAt(int index) {
		if (index < programs.length) {
			return TimeUtils.convertMillisToSeconds(programs[index].getPhaseShift());
		} else {
			int checkedInts = programs.length;

			for (int i = 0; i < programs.length; ++i)
			{
        Program program = programs[i];
				int noPhases = program.getPhasesCount();
				
				if (index < checkedInts + noPhases)
				{
					ProgramPhase phase = program.getPhase(index - checkedInts);
					
					return TimeUtils.convertMillisToSeconds(phase.getDuration());
				}
				
				checkedInts += noPhases;
			}
		}

		return null;
	}
	
	private void setProgramVariableAt(int index, int valueInSeconds) {
		if (index < programs.length) {
			// otherwise throws an exception
			if (valueInSeconds >= 0) {
				programs[index].setPhaseShift(
						TimeUtils.convertSecondsToMillis(valueInSeconds)
							% programs[index].getProgramLength());
//				programs[index].setPhaseShift(0L);
			}
		} else {
			
			if (valueInSeconds < MIN_PHASE_LENGTH) {
				valueInSeconds = MIN_PHASE_LENGTH;
			} else if (valueInSeconds > MAX_PHASE_LENGTH) {
				valueInSeconds = MAX_PHASE_LENGTH;
			}
			
			int checkedInts = programs.length;

			for (int i = 0; i < programs.length; ++i)
			{
        Program program = programs[i];
				int noPhases = program.getPhasesCount();
				
				if (index < checkedInts + noPhases - 1)
				{
					ProgramPhase phase = program.getPhase(index - checkedInts);
					
					int currentPhaseDuration = TimeUtils.convertMillisToSeconds(phase.getDuration());
					
					int changeOfLastPhaseDuration = computeChangeOfPhaseDuration(
							currentPhaseDuration,
							valueInSeconds);
					
					int lastPhaseDurationAfterChange = getLastPhaseDurationInSecondsAfterChange(
							program,
							changeOfLastPhaseDuration);
					
					phase.setDuration(TimeUtils.convertSecondsToMillis(valueInSeconds));
					
					program.getLastPhase().setDuration(TimeUtils.convertSecondsToMillis(lastPhaseDurationAfterChange));
					
					break; // done
				} else if (index == checkedInts + noPhases - 1) {
					break; // last phase is computed so we've done
				}
				
				checkedInts += noPhases;
			}
		}
	}
	
	public int getProgramsSize() {
		return programs.length + countPhasesForPrograms();
	}
	
	/**
	 * Phases lengths are decreased by
	 * {@link ProgramedTrafficNetIndividual#MIN_PHASE_LENGTH} before returning a
	 * bit.
	 */
	@Override
	public Integer get(int index)
	{
		int integerIndex = index / ProgramedTrafficNetIndividual.BITS_PER_INTEGER;
		int whichBit = index % ProgramedTrafficNetIndividual.BITS_PER_INTEGER;
		
		if (integerIndex < countPhasesShifts()) 
		{
			int shift = getShift(integerIndex);
			return getEncodedBit(shift, whichBit);
		} 
		else 
		{
			int checkedInts = countPhasesShifts();
			
			for (Program program : programs) 
			{
				int noPhases = program.getPhasesCount();
				if (integerIndex < checkedInts + noPhases - 1) 
				{
					ProgramPhase phase = program.getPhase(integerIndex - checkedInts);
					
					return getEncodedBit(
							TimeUtils.convertMillisToSeconds(phase.getDuration()) - MIN_PHASE_LENGTH, 
							whichBit);
				}
				
				checkedInts += noPhases - 1;
			}
		}
		return null;
	}

	private int getEncodedBit(int number, int whichBit) {
		return (number >> whichBit) & 0x1;
	}

	private int getShift(int whichShift) {
		return TimeUtils.convertMillisToSeconds(programs[whichShift].getPhaseShift());
	}

	@Override
	public void set(int index, Integer chromosomeBit) 
	{
		int integerIndex = index / ProgramedTrafficNetIndividual.BITS_PER_INTEGER;
		int whichBit = index % ProgramedTrafficNetIndividual.BITS_PER_INTEGER;
		
		if (integerIndex < countPhasesShifts()) 
		{
			int shift = getShift(integerIndex);
			Integer switched = switchBitInValue(shift, whichBit, chromosomeBit)
					% TimeUtils.convertMillisToSeconds(programs[integerIndex]
							.getProgramLength()); // preventing too large shift 
			setShiftAtIndexToValueInSeconds(integerIndex, switched);
		} 
		else 
		{
			int checkedInts = countPhasesShifts();
			
			for (Program program : programs) 
			{
				int noPhases = program.getPhasesCount();
				if (integerIndex < checkedInts + noPhases - 1) // we don't set the length of last phase 
				{                                              // it should be computed
					ProgramPhase phase = program.getPhase(integerIndex - checkedInts);
					
					int currentPhaseDuration = TimeUtils.convertMillisToSeconds(phase.getDuration());
					
					Integer switched = switchBitInValue(
							currentPhaseDuration - MIN_PHASE_LENGTH, 
							whichBit,
							chromosomeBit) + MIN_PHASE_LENGTH;
					
					int changeOfLastPhaseDuration = computeChangeOfPhaseDuration(
							currentPhaseDuration,
							switched);
					
					int lastPhaseDurationAfterChange = getLastPhaseDurationInSecondsAfterChange(
							program,
							changeOfLastPhaseDuration);
					
					phase.setDuration(TimeUtils.convertSecondsToMillis(switched));
					program.getLastPhase().setDuration(TimeUtils.convertSecondsToMillis(lastPhaseDurationAfterChange));
					
					break; // done
				}
				
				checkedInts += noPhases - 1;
			}
		}
	}

	private int getLastPhaseDurationInSecondsAfterChange(
			Program program,
			int changeBy) 
	{
		return TimeUtils.convertMillisToSeconds(program.getLastPhase().getDuration()) + changeBy;
	}

	private int computeChangeOfPhaseDuration(
			int currentDuration,
			int switched) 
	{
		return currentDuration - switched;
	}

	public void setShiftAtIndexToValueInSeconds(int index, Integer seconds)
	{
		programs[index].setPhaseShift(TimeUtils.convertSecondsToMillis(seconds));
	}

	private Integer switchBitInValue(Integer value, int whichBit, int bitToSet)
	{
		int bitSet = getEncodedBit(value, whichBit);
		
		Integer changedValue = null;
		if (bitToSet == bitSet)
		{
			changedValue =  value;
		}
		else if (bitToSet > bitSet)
		{
			changedValue = value + (0x1 << whichBit);
		}
		else if (bitToSet < bitSet)
		{
			changedValue = value - (0x1 << whichBit);
		}
		return changedValue;
	}

	/**
	 * Number of bits in programs coding in the traffic net.
	 */
	@Override
	public int size() {
		return ProgramedTrafficNetIndividual.BITS_PER_INTEGER
				* (countPhasesForPrograms() - 1); // we don't parametrize last phases
																					// and last program's shift
	}

	private int countPhasesForPrograms() {
		int pC = phasesCount;

		if (pC == 0) {
			for (Program program : programs) {
				pC += program.getPhasesCount();
			}
			phasesCount = pC;
		}

		return pC;
	}

	private int countPhasesShifts() {
		return programs.length - 1; // excluding the last one
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() 
	{
		return Arrays.toString(programs);
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object other) {
		if (this == other) {
			return true;
		}
		
		if (!(other instanceof ProgramedTrafficNetIndividual)) {
			return false;
		}
		
		ProgramedTrafficNetIndividual otherA = (ProgramedTrafficNetIndividual) other;

		return Arrays.equals(programs, otherA.programs);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() 
	{
		return Arrays.hashCode(programs);
	}

	/**
	 * @return the programs
	 */
	public Program[] getPrograms() {
		return programs;
	}
	
	public boolean isFeasibleSolution()
	{
		for (Program program : programs)
		{
			if (!isFeasibleProgram(program)) {
				return false;
			}
		}
		
		return true;
	}
	
	private boolean isFeasibleProgram(Program program)
	{
		if (!isFeasibleShift(program, 
					TimeUtils.convertMillisToSeconds(program.getPhaseShift()))) {
			return false;
		}
		
		int programLength = 0;
		
		Iterator<ProgramPhase> phaseIterator = program.getPhaseIterator();
		while (phaseIterator.hasNext()) {
			ProgramPhase programPhase = phaseIterator.next();
			
			int phaseLength = TimeUtils.convertMillisToSeconds(programPhase
					.getDuration());
			
			if (!isFeasiblePhaseLength(phaseLength)) {
				return false;
			}
			
			programLength += phaseLength;
		}
		
		if (programLength != TimeUtils.convertMillisToSeconds(program
				.getProgramLength())) {
			return false;
		}
		
		return true;
	}
	
	private boolean isFeasibleShift(Program p, Integer seconds) {
		return seconds >= 0 
				&& seconds <= TimeUtils.convertMillisToSeconds(p.getProgramLength());
	}
	
	private boolean isFeasiblePhaseLength(int phaseDurationSeconds) {
		return phaseDurationSeconds >= MIN_PHASE_LENGTH
						&& phaseDurationSeconds <= MAX_PHASE_LENGTH;
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#clone()
	 */
	@Override
	public ProgramedTrafficNetIndividual clone() throws CloneNotSupportedException {
		ProgramedTrafficNetIndividual clone = 
			(ProgramedTrafficNetIndividual) super.clone();
		
		clone.programs = new Program[programs.length];
		
		for (int i = 0; i < programs.length; ++i) {
			clone.programs[i] = programs[i].clone();
			clone.programs[i].setProgramLength(TimeUtils.convertSecondsToMillis(getMaxProgramLength(programs[i])));
		}
		return clone;
	}
	
	/**
	 * Realizes uniform cross over for two individuals.
	 * 
	 * @param ind not null individual to crossover with current
	 * @return Array of two not null crossed individuals
	 */
	public TrafficNetIndividual[] crossOver(
			TrafficNetIndividual individual)
	{
		assert individual != null : "Crossed individual cannot be empty!";
		
		ProgramedTrafficNetIndividual ind = (ProgramedTrafficNetIndividual) individual;
		
		ProgramedTrafficNetIndividual[] crossed = new ProgramedTrafficNetIndividual[2];
		
		ProgramedTrafficNetIndividual crossed1 = null;
		ProgramedTrafficNetIndividual crossed2 = null;
		try {
			crossed1 = this.clone();
			crossed2 = ind.clone();
		} catch (CloneNotSupportedException e) {
			LOGGER.severe("Couldn't clone individual.\n" + e);
			throw new IllegalStateException(e);
		}
		
    for (int i = 0; i < crossed1.getProgramsSize(); i++)
    {
      if (RandomFactory.getNextDouble() < 0.5d)
      {
      	int var1 = crossed1.getProgramVariableAt(i);
      	int var2 = crossed2.getProgramVariableAt(i);
      	
				crossed1.setProgramVariableAt(i, var2);
        crossed2.setProgramVariableAt(i, var1);
      }
    }
		
    crossed1.heal();
    crossed2.heal();
    
		crossed[0] = crossed1;
		crossed[1] = crossed2;
		
		return crossed;
	}

	/**
	 * Mutates random progams net variable (other than last phase).
	 */
	@Override
	public void mutate() {

		int index = 0;
		// draw index to mutate, but deny to mutate last phase
		int lastPhaseIndex = programs.length - 1;
		do {
			index = RandomFactory.getNextInt(getProgramsSize());
		} while (index == lastPhaseIndex);
		
		int var = getProgramVariableAt(index);
		
		if (RandomFactory.getNextBoolean()) {
  		// add 1
  		setProgramVariableAt(index, var + 1);
  	} else {
  		// substract 1
  		setProgramVariableAt(index, var - 1);
  	}
	}
	
	@Override
	public void heal() {
		
		if (!isFeasibleSolution()) {

			for (Program p : programs) {
				// TODO take care of shift -- later 
				
				// TODO take care of phases lengths -- more urgent (<MAX >MIN) 
				
				// especially last phase -- most urgent problem
				int lastPhaseDuration = TimeUtils.convertMillisToSeconds(p.getLastPhase().getDuration());
				
				if (!isFeasiblePhaseLength(lastPhaseDuration)) {
					if (lastPhaseDuration < MIN_PHASE_LENGTH) {
						int secondsToRelocate = MIN_PHASE_LENGTH - lastPhaseDuration;
						
						Iterator<ProgramPhase> iter = p.getPhaseIterator();
						
						List<ProgramPhase> list = new ArrayList<ProgramPhase>();
						while (iter.hasNext()) {
							ProgramPhase programPhase = iter.next();
							
							list.add(programPhase);
						}
						// removed last phase
						list.remove(list.size() - 1);
						
						Collections.sort(list, new Comparator<ProgramPhase>() {

							@Override
							public int compare(ProgramPhase o1, ProgramPhase o2) {
								if (o1.getDuration() > o2.getDuration()) {
									return 1;
								} else if (o1.getDuration() < o2.getDuration()) {
									return -1;
								}
								return 0;
							}
							
						});
						
						int sumSec = 0;
						
						for (ProgramPhase programPhase : list)
						{
							int phaseLen = TimeUtils.convertMillisToSeconds(programPhase.getDuration());
							
							float ratio = ((float) phaseLen)
									/ ((float) (TimeUtils.convertMillisToSeconds(p
											.getProgramLength()) - lastPhaseDuration));
							
							int secToSub = (int)((secondsToRelocate) * ratio);
							
							if (phaseLen - secToSub < MIN_PHASE_LENGTH)
							{
								sumSec += phaseLen - MIN_PHASE_LENGTH;
								phaseLen = MIN_PHASE_LENGTH;
							}
							else
							{
								sumSec += secToSub;
								phaseLen = phaseLen - secToSub;
							}
							
							programPhase.setDuration(TimeUtils.convertSecondsToMillis(phaseLen));
							
						}
						
						// roundings handle
						ProgramPhase maxPhase = list.get(list.size() - 1);
						maxPhase.setDuration(maxPhase.getDuration()
								- TimeUtils.convertSecondsToMillis(secondsToRelocate - sumSec));
						
						p.getLastPhase().setDuration(TimeUtils.convertSecondsToMillis(MIN_PHASE_LENGTH));
						
						if (LOGGER.isLoggable(Level.FINE)) {
							StringBuilder builder = new StringBuilder();
							builder.append("Fixed last phase of program: ");
							builder.append(p);
							LOGGER.fine(builder.toString());
						}
					}
				}
			}
		}
	}
	
	/**
	 * <code>(p.getPhasesCount() - 1) * MIN_PHASE_LENGTH + MAX_PHASE_LENGTH</code>
	 * 
	 * @param p
	 * @return
	 *         <code>(p.getPhasesCount() - 1) * MIN_PHASE_LENGTH + MAX_PHASE_LENGTH</code>
	 */
	private int getMaxProgramLength(Program p) {
		return (p.getPhasesCount() - 1) * MIN_PHASE_LENGTH + MAX_PHASE_LENGTH;
	}
}
